ネストされたナビゲーション フローを作成する
アプリは時間の経過とともに数十、さらには数百のルートを蓄積します。
ルートの中には、トップレベル (グローバル) ルートとして意味のあるものもあります。
たとえば、「/」、「プロフィール」、「連絡先」、「ソーシャルフィード」はすべて
アプリ内の可能なトップレベルのルート。
しかし、考えられるすべてのルートを
トップレベルNavigator
ウィジェット。リストは非常に長くなりますが、
そして、これらのルートの多くは、
別のウィジェット内でネストして処理する方が適切です。
ワイヤレスのモノのインターネット (IoT) セットアップ フローを検討します。
アプリで制御する電球。
このセットアップ フローは 4 ページで構成されます。
近くの電球を検索し、追加する電球を選択します。
電球を追加してセットアップを完了します。
この動作をトップレベルから調整できます。Navigator
ウィジェット。ただし、2 番目を定義する方が合理的です。
入れ子になったNavigator
ウィジェット内のSetupFlow
ウィジェット、
そしてネストさせますNavigator
4 ページの所有権を取得します
セットアップフローで。このナビゲーションの委任により、
ローカルコントロールの強化、つまり
一般に、ソフトウェアを開発する場合に推奨されます。
次のアニメーションはアプリの動作を示しています。
このレシピでは、4 ページの IoT セットアップを実装します。
下にネストされた独自のナビゲーションを維持するフロー
トップレベルのNavigator
ウィジェット。
ナビゲーションの準備をする
この IoT アプリには 2 つのトップレベル画面があります。 セットアップの流れと一緒に。これらを定義します ルート名を定数として使用できるようにする コード内で参照できます。
const routeHome = '/';
const routeSettings = '/settings';
const routePrefixDeviceSetup = '/setup/';
const routeDeviceSetupStart = '/setup/$routeDeviceSetupStartPage';
const routeDeviceSetupStartPage = 'find_devices';
const routeDeviceSetupSelectDevicePage = 'select_device';
const routeDeviceSetupConnectingPage = 'connecting';
const routeDeviceSetupFinishedPage = 'finished';
ホーム画面と設定画面は次のように参照されます。
静的な名前。ただし、セットアップ フロー ページでは、
2 つのパスを使用してルート名を作成します。
ある/setup/
プレフィックスの後に特定のページの名前が続きます。
2 つのパスを組み合わせることで、Navigator
判断できる
ルート名がセットアップ フローを対象としていることを示します。
に関連付けられたすべての個々のページを認識する
セットアップの流れ。
トップレベルNavigator
特定する責任はありません
個別のセットアップ フロー ページ。したがって、あなたのトップレベルは、Navigator
受信ルート名を解析する必要がある
セットアップ フローのプレフィックスを識別します。ルート名を解析する必要がある
は使用できないことを意味しますroutes
最上位のプロパティNavigator
。代わりに、次の関数を提供する必要があります。onGenerateRoute
財産。
埋め込むonGenerateRoute
適切なウィジェットを返すには
3 つのトップレベルのパスごとに。
onGenerateRoute: (settings) {
late Widget page;
if (settings.name == routeHome) {
page = const HomeScreen();
} else if (settings.name == routeSettings) {
page = const SettingsScreen();
} else if (settings.name!.startsWith(routePrefixDeviceSetup)) {
final subRoute =
settings.name!.substring(routePrefixDeviceSetup.length);
page = SetupFlow(
setupPageRoute: subRoute,
);
} else {
throw Exception('Unknown route: ${settings.name}');
}
return MaterialPageRoute<dynamic>(
builder: (context) {
return page;
},
settings: settings,
);
},
ホームルートと設定ルートが正確に一致していることに注目してください。
路線名。ただし、設定された流路条件のみ
プレフィックスをチェックします。ルート名にセットアップが含まれている場合
フロープレフィックスを追加した場合、ルート名の残りの部分は無視されます
そして、に渡されましたSetupFlow
処理するウィジェット。
このルート名の分割により、トップレベルのNavigator
さまざまなサブルートにとらわれないこと
セットアップ フロー内で。
というステートフル ウィジェットを作成します。SetupFlow
それか
ルート名を受け入れます。
class SetupFlow extends StatefulWidget {
const SetupFlow({
super.key,
required this.setupPageRoute,
});
final String setupPageRoute;
@override
SetupFlowState createState() => SetupFlowState();
}
class SetupFlowState extends State<SetupFlow> {
//...
}
セットアップフローのアプリバーを表示する
セットアップ フローでは永続的なアプリ バーが表示されます すべてのページに表示されます。
を返すScaffold
あなたのウィジェットSetupFlow
ウィジェットのbuild()
方法、
希望するものを含めますAppBar
ウィジェット。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildFlowAppBar(),
body: const SizedBox(),
);
}
PreferredSizeWidget _buildFlowAppBar() {
return AppBar(
title: const Text('Bulb Setup'),
);
}
アプリバーに戻る矢印が表示され、セットアップが終了します 戻る矢印を押すと流れます。しかし、 フローを終了すると、ユーザーはすべての進行状況を失います。 したがって、ユーザーは、次のことを行うかどうかを確認するように求められます。 セットアップフローを終了したい。
セットアップ フローを終了することをユーザーに確認するプロンプトを表示します。 ユーザーが次の操作を行ったときにプロンプトが表示されることを確認します。 Android のハードウェアの戻るボタンを押します。
Future<void> _onExitPressed() async {
final isConfirmed = await _isExitDesired();
if (isConfirmed && mounted) {
_exitSetup();
}
}
Future<bool> _isExitDesired() async {
return await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'If you exit device setup, your progress will be lost.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text('Leave'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Stay'),
),
],
);
}) ??
false;
}
void _exitSetup() {
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _isExitDesired,
child: Scaffold(
appBar: _buildFlowAppBar(),
body: const SizedBox(),
),
);
}
PreferredSizeWidget _buildFlowAppBar() {
return AppBar(
leading: IconButton(
onPressed: _onExitPressed,
icon: const Icon(Icons.chevron_left),
),
title: const Text('Bulb Setup'),
);
}
ユーザーがアプリバーの戻る矢印をタップすると、 または Android の場合は戻るボタンを押します。 警告ダイアログがポップアップ表示され、 ユーザーがセットアップ フローを終了したいと考えています。 ユーザーが押した場合離れるそうすると、セットアップ フローが自動的に起動します。 最上位のナビゲーション スタックから。 ユーザーが押した場合止まるの場合、アクションは無視されます。
お気づきかもしれませんが、Navigator.pop()
両方によって呼び出されます離れると止まるボタン。明確に言うと、
これpop()
アクションにより警告ダイアログがポップアップ表示されます
セットアップ フローではなく、ナビゲーション スタックです。
ネストされたルートを生成する
セットアップ フローの仕事は、適切な設定を表示することです。 フロー内のページ。
追加Navigator
ウィジェットへSetupFlow
、
そして実装してくださいonGenerateRoute
財産。
final _navigatorKey = GlobalKey<NavigatorState>();
void _onDiscoveryComplete() {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupSelectDevicePage);
}
void _onDeviceSelected(String deviceId) {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupConnectingPage);
}
void _onConnectionEstablished() {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupFinishedPage);
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _isExitDesired,
child: Scaffold(
appBar: _buildFlowAppBar(),
body: Navigator(
key: _navigatorKey,
initialRoute: widget.setupPageRoute,
onGenerateRoute: _onGenerateRoute,
),
),
);
}
Route _onGenerateRoute(RouteSettings settings) {
late Widget page;
switch (settings.name) {
case routeDeviceSetupStartPage:
page = WaitingPage(
message: 'Searching for nearby bulb...',
onWaitComplete: _onDiscoveryComplete,
);
break;
case routeDeviceSetupSelectDevicePage:
page = SelectDevicePage(
onDeviceSelected: _onDeviceSelected,
);
break;
case routeDeviceSetupConnectingPage:
page = WaitingPage(
message: 'Connecting...',
onWaitComplete: _onConnectionEstablished,
);
break;
case routeDeviceSetupFinishedPage:
page = FinishedPage(
onFinishPressed: _exitSetup,
);
break;
}
return MaterialPageRoute<dynamic>(
builder: (context) {
return page;
},
settings: settings,
);
}
の_onGenerateRoute
関数は次と同じように動作します
トップレベルの場合Navigator
。あRouteSettings
オブジェクトが関数に渡されると、
これにはルートが含まれますname
。
その路線名からすると、
4 つのフロー ページのうちの 1 つが返されます。
最初のページと呼ばれるfind_devices
、
ネットワーク スキャンをシミュレートするために数秒待ちます。
待機期間の後、ページはコールバックを呼び出します。
この場合、そのコールバックは_onDiscoveryComplete
。
セットアップ フローは、デバイスの検出時にそれを認識します。
完了すると、デバイス選択ページが表示されます。
したがって、_onDiscoveryComplete
、_navigatorKey
ネストされたものに指示しますNavigator
に移動するにはselect_device
ページ。
のselect_device
ページはユーザーに選択を求めます
利用可能なデバイスのリストからデバイスを選択します。このレシピでは、
ユーザーに提示されるデバイスは 1 つだけです。
ユーザーがデバイスをタップすると、onDeviceSelected
コールバックが呼び出されます。セットアップ フローでは次のことが認識されます。
デバイスを選択すると、接続ページが表示されます
示されるべきである。したがって、_onDeviceSelected
、
の_navigatorKey
ネストされたものに指示しますNavigator
に移動するには”connecting”
ページ。
のconnecting
ページは と同じように機能しますfind_devices
ページ。のconnecting
ページ待機
数秒間呼び出した後、コールバックを呼び出します。
この場合、コールバックは次のようになります。_onConnectionEstablished
。
セットアップ フローは、接続が確立されると、
最後のページが表示されるはずです。したがって、
の_onConnectionEstablished
、_navigatorKey
ネストされたものに指示しますNavigator
に移動するにはfinished
ページ。
のfinished
ページはユーザーに終了ボタン。ユーザーがタップすると終了、
の_exitSetup
コールバックが呼び出され、全体がポップされます。
トップレベルからのセットアップ フローNavigator
スタック、
ユーザーをホーム画面に戻します。
おめでとう! 4 つのサブルートを含むネストされたナビゲーションを実装しました。
インタラクティブな例
アプリを実行します。
- で最初の電球を追加します画面、 プラス記号で表示されている FAB をクリックします。+。 これにより、近くのデバイスを選択してください画面。単一の電球がリストされています。
- リストされている電球をクリックします。あ終了した!画面が表示されます。
- クリック終了したに戻るボタン 最初の画面。
import 'package:flutter/material.dart';
const routeHome = '/';
const routeSettings = '/settings';
const routePrefixDeviceSetup = '/setup/';
const routeDeviceSetupStart = '/setup/$routeDeviceSetupStartPage';
const routeDeviceSetupStartPage = 'find_devices';
const routeDeviceSetupSelectDevicePage = 'select_device';
const routeDeviceSetupConnectingPage = 'connecting';
const routeDeviceSetupFinishedPage = 'finished';
void main() {
runApp(
MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Colors.blue,
),
),
onGenerateRoute: (settings) {
late Widget page;
if (settings.name == routeHome) {
page = const HomeScreen();
} else if (settings.name == routeSettings) {
page = const SettingsScreen();
} else if (settings.name!.startsWith(routePrefixDeviceSetup)) {
final subRoute =
settings.name!.substring(routePrefixDeviceSetup.length);
page = SetupFlow(
setupPageRoute: subRoute,
);
} else {
throw Exception('Unknown route: ${settings.name}');
}
return MaterialPageRoute<dynamic>(
builder: (context) {
return page;
},
settings: settings,
);
},
debugShowCheckedModeBanner: false,
),
);
}
@immutable
class SetupFlow extends StatefulWidget {
static SetupFlowState of(BuildContext context) {
return context.findAncestorStateOfType<SetupFlowState>()!;
}
const SetupFlow({
super.key,
required this.setupPageRoute,
});
final String setupPageRoute;
@override
SetupFlowState createState() => SetupFlowState();
}
class SetupFlowState extends State<SetupFlow> {
final _navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
}
void _onDiscoveryComplete() {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupSelectDevicePage);
}
void _onDeviceSelected(String deviceId) {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupConnectingPage);
}
void _onConnectionEstablished() {
_navigatorKey.currentState!.pushNamed(routeDeviceSetupFinishedPage);
}
Future<void> _onExitPressed() async {
final isConfirmed = await _isExitDesired();
if (isConfirmed && mounted) {
_exitSetup();
}
}
Future<bool> _isExitDesired() async {
return await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'If you exit device setup, your progress will be lost.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text('Leave'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Stay'),
),
],
);
}) ??
false;
}
void _exitSetup() {
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _isExitDesired,
child: Scaffold(
appBar: _buildFlowAppBar(),
body: Navigator(
key: _navigatorKey,
initialRoute: widget.setupPageRoute,
onGenerateRoute: _onGenerateRoute,
),
),
);
}
Route _onGenerateRoute(RouteSettings settings) {
late Widget page;
switch (settings.name) {
case routeDeviceSetupStartPage:
page = WaitingPage(
message: 'Searching for nearby bulb...',
onWaitComplete: _onDiscoveryComplete,
);
break;
case routeDeviceSetupSelectDevicePage:
page = SelectDevicePage(
onDeviceSelected: _onDeviceSelected,
);
break;
case routeDeviceSetupConnectingPage:
page = WaitingPage(
message: 'Connecting...',
onWaitComplete: _onConnectionEstablished,
);
break;
case routeDeviceSetupFinishedPage:
page = FinishedPage(
onFinishPressed: _exitSetup,
);
break;
}
return MaterialPageRoute<dynamic>(
builder: (context) {
return page;
},
settings: settings,
);
}
PreferredSizeWidget _buildFlowAppBar() {
return AppBar(
leading: IconButton(
onPressed: _onExitPressed,
icon: const Icon(Icons.chevron_left),
),
title: const Text('Bulb Setup'),
);
}
}
class SelectDevicePage extends StatelessWidget {
const SelectDevicePage({
super.key,
required this.onDeviceSelected,
});
final void Function(String deviceId) onDeviceSelected;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Select a nearby device:',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 54,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateColor.resolveWith((states) {
return const Color(0xFF222222);
}),
),
onPressed: () {
onDeviceSelected('22n483nk5834');
},
child: const Text(
'Bulb 22n483nk5834',
style: TextStyle(
fontSize: 24,
),
),
),
),
],
),
),
),
);
}
}
class WaitingPage extends StatefulWidget {
const WaitingPage({
super.key,
required this.message,
required this.onWaitComplete,
});
final String message;
final VoidCallback onWaitComplete;
@override
State<WaitingPage> createState() => _WaitingPageState();
}
class _WaitingPageState extends State<WaitingPage> {
@override
void initState() {
super.initState();
_startWaiting();
}
Future<void> _startWaiting() async {
await Future<dynamic>.delayed(const Duration(seconds: 3));
if (mounted) {
widget.onWaitComplete();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 32),
Text(widget.message),
],
),
),
),
);
}
}
class FinishedPage extends StatelessWidget {
const FinishedPage({
super.key,
required this.onFinishPressed,
});
final VoidCallback onFinishPressed;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 250,
height: 250,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF222222),
),
child: const Center(
child: Icon(
Icons.lightbulb,
size: 175,
color: Colors.white,
),
),
),
const SizedBox(height: 32),
const Text(
'Bulb added!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 32),
ElevatedButton(
style: ButtonStyle(
padding: MaterialStateProperty.resolveWith((states) {
return const EdgeInsets.symmetric(
horizontal: 24, vertical: 12);
}),
backgroundColor: MaterialStateColor.resolveWith((states) {
return const Color(0xFF222222);
}),
shape: MaterialStateProperty.resolveWith((states) {
return const StadiumBorder();
}),
),
onPressed: onFinishPressed,
child: const Text(
'Finish',
style: TextStyle(
fontSize: 24,
),
),
),
],
),
),
),
);
}
}
@immutable
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(context),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 250,
height: 250,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF222222),
),
child: Center(
child: Icon(
Icons.lightbulb,
size: 175,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
const SizedBox(height: 32),
const Text(
'Add your first bulb',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed(routeDeviceSetupStart);
},
child: const Icon(Icons.add),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
title: const Text('Welcome'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, routeSettings);
},
),
],
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(8, (index) {
return Container(
width: double.infinity,
height: 54,
margin: const EdgeInsets.only(left: 16, right: 16, top: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: const Color(0xFF222222),
),
);
}),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: const Text('Settings'),
);
}
}